// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == "object" && typeof module == "object")
    // CommonJS
    mod(require("codemirror"));
  // eslint-disable-next-line no-undef
  else if (typeof define == "function" && define.amd)
    // AMD
    // eslint-disable-next-line no-undef
    define(["codemirror"], mod);
  // Plain browser env
  // eslint-disable-next-line no-undef
  else mod(CodeMirror);
})(function(CodeMirror) {
  "use strict";

  CodeMirror.defineMode("sql", function(config, parserConfig) {
    "use strict";

    var client = parserConfig.client || {},
      atoms = parserConfig.atoms || { false: true, true: true, null: true },
      builtin = parserConfig.builtin || {},
      keywords = parserConfig.keywords || {},
      operatorChars = parserConfig.operatorChars || /^[*+\-%<>!=&|~^]/,
      support = parserConfig.support || {},
      hooks = parserConfig.hooks || {},
      dateSQL = parserConfig.dateSQL || {
        date: true,
        time: true,
        timestamp: true
      },
      backslashStringEscapes = parserConfig.backslashStringEscapes !== false,
      // eslint-disable-next-line no-useless-escape
      brackets = parserConfig.brackets || /^[\{}\(\)\[\]]/,
      punctuation = parserConfig.punctuation || /^[;.,:]/;

    function tokenBase(stream, state) {
      var ch = stream.next();

      // call hooks from the mime type
      if (hooks[ch]) {
        var result = hooks[ch](stream, state);
        if (result !== false) return result;
      }

      if (support.hexNumber && ((ch == "0" && stream.match(/^[xX][0-9a-fA-F]+/)) || ((ch == "x" || ch == "X") && stream.match(/^'[0-9a-fA-F]+'/)))) {
        // hex
        // ref: http://dev.mysql.com/doc/refman/5.5/en/hexadecimal-literals.html
        return "number";
      } else if (support.binaryNumber && (((ch == "b" || ch == "B") && stream.match(/^'[01]+'/)) || (ch == "0" && stream.match(/^b[01]+/)))) {
        // bitstring
        // ref: http://dev.mysql.com/doc/refman/5.5/en/bit-field-literals.html
        return "number";
      } else if (ch.charCodeAt(0) > 47 && ch.charCodeAt(0) < 58) {
        // numbers
        // ref: http://dev.mysql.com/doc/refman/5.5/en/number-literals.html
        stream.match(/^[0-9]*(\.[0-9]+)?([eE][-+]?[0-9]+)?/);
        support.decimallessFloat && stream.match(/^\.(?!\.)/);
        return "number";
      } else if (ch == "?" && (stream.eatSpace() || stream.eol() || stream.eat(";"))) {
        // placeholders
        return "variable-3";
      } else if (ch == "'" || (ch == '"' && support.doubleQuote)) {
        // strings
        // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html
        state.tokenize = tokenLiteral(ch);
        return state.tokenize(stream, state);
      } else if (((support.nCharCast && (ch == "n" || ch == "N")) || (support.charsetCast && ch == "_" && stream.match(/[a-z][a-z0-9]*/i))) && (stream.peek() == "'" || stream.peek() == '"')) {
        // charset casting: _utf8'str', N'str', n'str'
        // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html
        return "keyword";
      } else if (support.commentSlashSlash && ch == "/" && stream.eat("/")) {
        // 1-line comment
        stream.skipToEnd();
        return "comment";
      } else if ((support.commentHash && ch == "#") || (ch == "-" && stream.eat("-") && (!support.commentSpaceRequired || stream.eat(" ")))) {
        // 1-line comments
        // ref: https://kb.askmonty.org/en/comment-syntax/
        stream.skipToEnd();
        return "comment";
      } else if (ch == "/" && stream.eat("*")) {
        // multi-line comments
        // ref: https://kb.askmonty.org/en/comment-syntax/
        state.tokenize = tokenComment(1);
        return state.tokenize(stream, state);
      } else if (ch == ".") {
        // .1 for 0.1
        if (support.zerolessFloat && stream.match(/^(?:\d+(?:e[+-]?\d+)?)/i)) return "number";
        if (stream.match(/^\.+/)) return null;
        // .table_name (ODBC)
        // // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html
        if (support.ODBCdotTable && stream.match(/^[\w\d_]+/)) return "variable-2";
      } else if (operatorChars.test(ch)) {
        // operators
        stream.eatWhile(operatorChars);
        return "operator";
      } else if (brackets.test(ch)) {
        // brackets
        stream.eatWhile(brackets);
        return "bracket";
      } else if (punctuation.test(ch)) {
        // punctuation
        stream.eatWhile(punctuation);
        return "punctuation";
      } else if (ch == "{" && (stream.match(/^( )*(d|D|t|T|ts|TS)( )*'[^']*'( )*}/) || stream.match(/^( )*(d|D|t|T|ts|TS)( )*"[^"]*"( )*}/))) {
        // dates (weird ODBC syntax)
        // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html
        return "number";
      } else {
        stream.eatWhile(/^[_\w\d]/);
        var word = stream.current().toLowerCase();
        // dates (standard SQL syntax)
        // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html
        if (
          // eslint-disable-next-line no-prototype-builtins
          dateSQL.hasOwnProperty(word) &&
          (stream.match(/^( )+'[^']*'/) || stream.match(/^( )+"[^"]*"/))
        )
          return "number";
        // eslint-disable-next-line no-prototype-builtins
        if (atoms.hasOwnProperty(word)) return "atom";
        // eslint-disable-next-line no-prototype-builtins
        if (builtin.hasOwnProperty(word)) return "builtin";
        // eslint-disable-next-line no-prototype-builtins
        if (keywords.hasOwnProperty(word)) return "keyword";
        // eslint-disable-next-line no-prototype-builtins
        if (client.hasOwnProperty(word)) return "string-2";
        return null;
      }
    }

    // 'string', with char specified in quote escaped by '\'
    function tokenLiteral(quote) {
      return function(stream, state) {
        var escaped = false,
          ch;
        while ((ch = stream.next()) != null) {
          if (ch == quote && !escaped) {
            state.tokenize = tokenBase;
            break;
          }
          escaped = backslashStringEscapes && !escaped && ch == "\\";
        }
        return "string";
      };
    }

    function tokenComment(depth) {
      return function(stream, state) {
        var m = stream.match(/^.*?(\/\*|\*\/)/);
        if (!m) stream.skipToEnd();
        else if (m[1] == "/*") state.tokenize = tokenComment(depth + 1);
        else if (depth > 1) state.tokenize = tokenComment(depth - 1);
        else state.tokenize = tokenBase;
        return "comment";
      };
    }

    function pushContext(stream, state, type) {
      state.context = {
        prev: state.context,
        indent: stream.indentation(),
        col: stream.column(),
        type: type
      };
    }

    function popContext(state) {
      state.indent = state.context.indent;
      state.context = state.context.prev;
    }

    return {
      startState: function() {
        return { tokenize: tokenBase, context: null };
      },

      token: function(stream, state) {
        if (stream.sol()) {
          if (state.context && state.context.align == null) state.context.align = false;
        }
        if (state.tokenize == tokenBase && stream.eatSpace()) return null;

        var style = state.tokenize(stream, state);
        if (style == "comment") return style;

        if (state.context && state.context.align == null) state.context.align = true;

        var tok = stream.current();
        if (tok == "(") pushContext(stream, state, ")");
        else if (tok == "[") pushContext(stream, state, "]");
        else if (state.context && state.context.type == tok) popContext(state);
        return style;
      },

      indent: function(state, textAfter) {
        var cx = state.context;
        if (!cx) return CodeMirror.Pass;
        var closing = textAfter.charAt(0) == cx.type;
        if (cx.align) return cx.col + (closing ? 0 : 1);
        else return cx.indent + (closing ? 0 : config.indentUnit);
      },

      blockCommentStart: "/*",
      blockCommentEnd: "*/",
      lineComment: support.commentSlashSlash ? "//" : support.commentHash ? "#" : "--",
      closeBrackets: "()[]{}''\"\"``"
    };
  });

  (function() {
    "use strict";

    // `identifier`
    // eslint-disable-next-line no-unused-vars
    function hookIdentifier(stream) {
      // MySQL/MariaDB identifiers
      // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html
      var ch;
      while ((ch = stream.next()) != null) {
        if (ch == "`" && !stream.eat("`")) return "variable-2";
      }
      stream.backUp(stream.current().length - 1);
      return stream.eatWhile(/\w/) ? "variable-2" : null;
    }

    // "identifier"
    // eslint-disable-next-line no-unused-vars
    function hookIdentifierDoublequote(stream) {
      // Standard SQL /SQLite identifiers
      // ref: http://web.archive.org/web/20160813185132/http://savage.net.au/SQL/sql-99.bnf.html#delimited%20identifier
      // ref: http://sqlite.org/lang_keywords.html
      var ch;
      while ((ch = stream.next()) != null) {
        if (ch == '"' && !stream.eat('"')) return "variable-2";
      }
      stream.backUp(stream.current().length - 1);
      return stream.eatWhile(/\w/) ? "variable-2" : null;
    }

    // variable token
    // eslint-disable-next-line no-unused-vars
    function hookVar(stream) {
      // variables
      // @@prefix.varName @varName
      // varName can be quoted with ` or ' or "
      // ref: http://dev.mysql.com/doc/refman/5.5/en/user-variables.html
      if (stream.eat("@")) {
        stream.match(/^session\./);
        stream.match(/^local\./);
        stream.match(/^global\./);
      }

      if (stream.eat("'")) {
        stream.match(/^.*'/);
        return "variable-2";
      } else if (stream.eat('"')) {
        stream.match(/^.*"/);
        return "variable-2";
      } else if (stream.eat("`")) {
        stream.match(/^.*`/);
        return "variable-2";
        // eslint-disable-next-line no-useless-escape
      } else if (stream.match(/^[0-9a-zA-Z$\.\_]+/)) {
        return "variable-2";
      }
      return null;
    }

    // short client keyword token
    // eslint-disable-next-line no-unused-vars
    function hookClient(stream) {
      // \N means NULL
      // ref: http://dev.mysql.com/doc/refman/5.5/en/null-values.html
      if (stream.eat("N")) {
        return "atom";
      }
      // \g, etc
      // ref: http://dev.mysql.com/doc/refman/5.5/en/mysql-commands.html
      return stream.match(/^[a-zA-Z.#!?]/) ? "variable-2" : null;
    }

    // these keywords are used by all SQL dialects (however, a mode can still overwrite it)
    // eslint-disable-next-line no-unused-vars
    var sqlKeywords =
      "alter and as asc between by count create delete desc distinct drop from group having in insert into is join like not on or order select set table union update values where limit ";

    // turn a space-separated list into an array
    function set(str) {
      var obj = {},
        words = str.split(" ");
      for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
      return obj;
    }

    // Spark SQL
    CodeMirror.defineMIME("text/stream-sql", {
      name: "sql",
      keywords: set(
        "add after all alter analyze and anti archive array as asc at between bucket buckets by cache cascade case cast change clear cluster clustered codegen collection column columns comment commit compact compactions compute concatenate cost create cross cube current current_date current_timestamp database databases datata dbproperties defined delete delimited desc describe dfs directories distinct distribute drop else end escaped except exchange exists explain export extended external false fields fileformat first following for format formatted from full function functions global grant group grouping having if ignore import in index indexes inner inpath inputformat insert intersect interval into is items join keys last lateral lazy left like limit lines list load local location lock locks logical macro map minus msck natural no not null nulls of on option options or order out outer outputformat over overwrite partition partitioned partitions percent preceding principals purge range recordreader recordwriter recover reduce refresh regexp rename repair replace reset restrict revoke right rlike role roles rollback rollup row rows schema schemas select semi separated serde serdeproperties set sets show skewed sort sorted start statistics stored stratify struct table tables tablesample tblproperties temp temporary terminated then to touch transaction transactions transform true truncate unarchive unbounded uncache union unlock unset use using values view when where window with"
      ),
      builtin: set("tinyint smallint int bigint boolean float double string binary timestamp decimal array map struct uniontype delimited serde sequencefile textfile rcfile inputformat outputformat"),
      atoms: set("false true null"),
      operatorChars: /^[*+\-%<>!=~&|^]/,
      dateSQL: set("date time timestamp"),
      support: set("ODBCdotTable doubleQuote zerolessFloat")
    });
  })();
});

/*
  How Properties of Mime Types are used by SQL Mode
  =================================================

  keywords:
    A list of keywords you want to be highlighted.
  builtin:
    A list of builtin types you want to be highlighted (if you want types to be of class "builtin" instead of "keyword").
  operatorChars:
    All characters that must be handled as operators.
  client:
    Commands parsed and executed by the client (not the server).
  support:
    A list of supported syntaxes which are not common, but are supported by more than 1 DBMS.
    * ODBCdotTable: .tableName
    * zerolessFloat: .1
    * doubleQuote
    * nCharCast: N'string'
    * charsetCast: _utf8'string'
    * commentHash: use # char for comments
    * commentSlashSlash: use // for comments
    * commentSpaceRequired: require a space after -- for comments
  atoms:
    Keywords that must be highlighted as atoms,. Some DBMS's support more atoms than others:
    UNKNOWN, INFINITY, UNDERFLOW, NaN...
  dateSQL:
    Used for date/time SQL standard syntax, because not all DBMS's support same temporal types.
*/
